using System;
using System.Collections.Generic;
using SR = System.Reflection;
using System.Text;

using Mono.Cecil;
using Mono.Cecil.Cil;

namespace brainfucker
{
    public static class Compiler
    {
        private static AssemblyDefinition assembly;
        private static TypeDefinition type;
        private static MethodDefinition readMethod;
        private static MethodDefinition queueArgumentsMethod;
        
        private static TypeReference Import(Type t)
        {
            return assembly.MainModule.Import(t);
        }
        private static MethodReference Import(SR.MethodInfo mi)
        {
            return assembly.MainModule.Import(mi);
        }
        
        private static AssemblyDefinition CompileNormal(string name, string source, bool ignore, bool endOfArgs)
        {
            MethodDefinition method = new MethodDefinition("main",
                MethodAttributes.Public|MethodAttributes.Static, Import(typeof(void)));
            method.Parameters.Add(new ParameterDefinition("args", 0, (ParameterAttributes)0, Import(typeof(string[]))));
            GenerateCIL(method, source, ignore, endOfArgs);
            type.Methods.Add(method);
            
            assembly.EntryPoint = method;
            return assembly;
        }
        
        private static AssemblyDefinition CompileInterpreter(string name, string source, bool endOfArgs)
        {
            System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
            byte[] data = encoding.GetBytes(source);
            
            EmbeddedResource res = new EmbeddedResource("source", (ManifestResourceAttributes)0, data);
            assembly.MainModule.Resources.Add(res);
            
            Import(typeof(System.Reflection.Assembly));
            
            MethodDefinition interpretMainMethod = null;
            
            string path = SR.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
            AssemblyDefinition thisAssembly = AssemblyFactory.GetAssembly(path);
            foreach(TypeDefinition t in thisAssembly.MainModule.Types) {
                if(t.Name == "InjectedInterpretMethods") {
                    TypeDefinition injectedType = type.Module.Inject(t);
                    interpretMainMethod = injectedType.Methods.GetMethod("InterpretMain")[0];
                    type.Module.Types.Add(injectedType);
                } else if(t.Name == "InterpreterReader" || t.Name == "Interpreter") {
                    type.Module.Types.Add(type.Module.Inject(t));
                }
            }
            
            MethodDefinition method = new MethodDefinition("main",
                MethodAttributes.Public|MethodAttributes.Static, Import(typeof(void)));
            method.Parameters.Add(new ParameterDefinition("args", 0, (ParameterAttributes)0, Import(typeof(string[]))));
            
            method.Body.CilWorker.Emit(OpCodes.Ldarg_0);
            method.Body.CilWorker.Emit(endOfArgs ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
            method.Body.CilWorker.Emit(OpCodes.Call, interpretMainMethod);
            method.Body.CilWorker.Emit(OpCodes.Ret);
            type.Methods.Add(method);
            
            assembly.EntryPoint = method;
            
            return assembly;
        }
        
        public static AssemblyDefinition Compile(string name, string source, bool ignore, bool intArray, bool endOfArgs, bool embed)
        {
            Compiler.intArray = intArray;
            assembly = AssemblyFactory.DefineAssembly(name, name + ".exe", TargetRuntime.NET_2_0, AssemblyKind.Console);
            
            type = new TypeDefinition("Program", "brainfucker",
                TypeAttributes.AutoClass|TypeAttributes.Public, Import(typeof(object)));
            assembly.MainModule.Types.Add(type);
            
            Import(typeof(int));
            Import(typeof(System.Collections.Generic.Queue<int>));
            Import(typeof(System.IO.File));
            
            string path = SR.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
            AssemblyDefinition thisAssembly = AssemblyFactory.GetAssembly(path);
            foreach(TypeDefinition t in thisAssembly.MainModule.Types) {
                if(t.Name == "InjectedMethods") {
                    TypeDefinition injectedType = type.Module.Inject(t);
                    
                    readMethod = injectedType.Methods.GetMethod("Read")[0];
                    queueArgumentsMethod = injectedType.Methods.GetMethod("QueueArguments")[0];

                    type.Module.Types.Add(injectedType);
                    break;
                }
            }
            return embed ? CompileInterpreter(name, source, endOfArgs) :
                CompileNormal(name, source, ignore, endOfArgs);
            
        }
        
        private static CilWorker worker;
        private static VariableDefinition array;
        private static VariableDefinition index;
        private static VariableDefinition args;
        
        private class Loop
        {
            public Instruction LoopStart;
            public Instruction ConditionStart;
        }
        
        private static int array_size = 1;
        private static int cell = 1;
        private static int array_start_offset = 0;
        private static int loop = 0;
        private static List<Loop> loopList = new List<Loop>();
        
        private static int crementValue;
        private static bool crementing;
        private static int moveValue;
        private static bool moving;
        
        private static char beginComment = ' ';
        private static bool endMultilineComment;
        private static bool inMultilineComment;
        private static bool inComment;
        
        private static bool intArray;
        
        private static void GenerateCIL(MethodDefinition md, string source, bool ignore, bool endOfArgs)
        {
            worker = md.Body.CilWorker;
            
            array = new VariableDefinition("array", 0, md, Import(intArray ? typeof(int[]) : typeof(byte[])));
            index = new VariableDefinition("index", 1, md, Import(typeof(int)));
            args = new VariableDefinition("args", 2, md, Import(intArray ? 
                typeof(System.Collections.Generic.Queue<int>) :
                typeof(System.Collections.Generic.Queue<byte>)));
            
            md.Body.Variables.Add(array);
            md.Body.Variables.Add(index);
            md.Body.Variables.Add(args);
            
            worker.Emit(OpCodes.Ldarg_0);
            worker.Emit(endOfArgs ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
            worker.Emit(OpCodes.Call, queueArgumentsMethod);
            worker.Emit(OpCodes.Stloc, args);

		    foreach(char c in source.ToCharArray()) {
		        bool commented = inComment || inMultilineComment;
		        if(beginComment != ' ') {
		            if(c == beginComment) {
		                inComment = true;
		            } else if(beginComment == '/' && c == '*') {
		                inMultilineComment = true;
		            }
		            beginComment = ' ';
		        }
		        if(endMultilineComment) {
		            if(c == '/') {
		                inMultilineComment = false;
		            }
		            endMultilineComment = false;
		        }
		        if((c == '/' || c == ';') && !inComment && !inMultilineComment) {
		            beginComment = c;
		        } else if(c == '*' && inMultilineComment) {
		            endMultilineComment = true;
		        } else if(c == '\n' && inComment) {
		            inComment = false;
		        } else if(c == '#' && !inComment && !inMultilineComment) {
		            inComment = true;
		        }
		        
		        if((commented || beginComment != ' ' || inComment || inMultilineComment) && !ignore) {
		            continue;
		        }
		        
		        // if a program ends in +, -, > or < the opperation will not occure, but who will know?
		        if(crementing && c != '+' && c != '-' && IsCommandChar(c)) {
		            if(crementValue > 256 || crementValue < -256) {
		                throw new CompilerException("Value overflow. Try compiling with --intArray.");
		            }
		            Crement(Math.Abs(crementValue), (crementValue < 0) ? false : true);
		            crementing = false;
		            crementValue = 0;
		        } else if(moving && c != '>' && c != '<' && IsCommandChar(c)) {
		            MovePointer(Math.Abs(moveValue), (moveValue < 0) ? false : true);
		            moving = false;
		            moveValue = 0;
		        }
		        switch(c) {
		        case '>':
		            if(cell == array_size) {
		                array_size++;
		            }
		            cell++;
		            moving = true;
		            moveValue++;
		            break;
		        case '<':
		            cell--;
		            if(cell == array_start_offset) {
		                array_size++;
		                array_start_offset--;
		            }
		            moving = true;
		            moveValue--;
		            break;
		        case '+':
		            crementValue++;
		            crementing = true;
		            break;
		        case '-':
		            crementValue--;
		            crementing = true;
		            break;
		        case '.': {
		            worker.Emit(OpCodes.Ldloc, array);
		            worker.Emit(OpCodes.Ldloc, index);
		            worker.Emit(intArray ? OpCodes.Ldelem_I4 : OpCodes.Ldelem_I1);
		            worker.Emit(OpCodes.Call, Import(typeof(Console).GetMethod("Write", new Type[]{typeof(char)})));
		            break;
		        }
		        case ',': {
		            worker.Emit(OpCodes.Ldloc, array);
		            worker.Emit(OpCodes.Ldloc, index);
		            worker.Emit(OpCodes.Ldloc, args);
		            worker.Emit(OpCodes.Call, readMethod);
		            worker.Emit(intArray ? OpCodes.Conv_I4 : OpCodes.Conv_U1);
		            worker.Emit(intArray ? OpCodes.Stelem_I4 : OpCodes.Stelem_I1);
		            break;
		        }
		        case '[': {
		            loop++;
		            Loop l = new Loop();
		            l.LoopStart = worker.Create(OpCodes.Nop);
		            worker.Append(l.LoopStart);
		            loopList.Add(l);
		            break;
		        }
		        case ']': {
		            loop--;
		            Loop l = null;
		            for(int i = loopList.Count - 1; i >= 0; i--) {
		                if(loopList[i].ConditionStart == null) {
		                    l = loopList[i];
		                    break;
		                }
		            }
		            if(l == null) {
		                throw new CompilerException("Syntax Error: Unopened loop");
		            }
		            
		            l.ConditionStart = worker.Create(OpCodes.Ldloc, array);
		            worker.Append(l.ConditionStart);
		            worker.Emit(OpCodes.Ldloc, index);
		            worker.Emit(intArray ? OpCodes.Ldelem_I4 : OpCodes.Ldelem_I1);
		            worker.Emit(OpCodes.Ldc_I4_0);
		            worker.Emit(OpCodes.Bgt, l.LoopStart);
		            
		            Instruction check = worker.Create(OpCodes.Br, l.ConditionStart);
		            worker.InsertBefore(l.LoopStart, check);
		            
		            break;
		        }
		        default:
		            if(c != ' ' && c != '\n' && c != '\t' && c != '@' && (int)c != 13 && !ignore) {
		                throw new CompilerException("Syntax Error");
		            }
		            break;
		        }
		    }
		    
		    if(loop != 0) {
		        throw new CompilerException("Syntax Error: Unclosed loop");
		    }
		    
		    Instruction pushSize = worker.Create(OpCodes.Ldc_I4, array_size);
		    Instruction createArray = worker.Create(OpCodes.Newarr, intArray ?
		        Import(typeof(int)) : Import(typeof(byte)));
		    Instruction storeArray = worker.Create(OpCodes.Stloc, array);
		    Instruction loadOffset = worker.Create(OpCodes.Ldc_I4, Math.Abs(array_start_offset));
		    Instruction storeOffset = worker.Create(OpCodes.Stloc, index);
		    
		    worker.InsertBefore(md.Body.Instructions[0], pushSize);
		    worker.InsertAfter(pushSize, createArray);
		    worker.InsertAfter(createArray, storeArray);
		    worker.InsertAfter(storeArray, loadOffset);
		    worker.InsertAfter(loadOffset, storeOffset);
		    
		    worker.Emit(OpCodes.Call, Import(typeof(Console).GetMethod("WriteLine", new Type[]{})));            
            worker.Emit(OpCodes.Ret);
        }
        
        private static void MovePointer(int ammount, bool forward)
        {
            worker.Emit(OpCodes.Ldloc, index);
            worker.Emit(OpCodes.Ldc_I4, ammount);
            worker.Emit(forward ? OpCodes.Add : OpCodes.Sub);
            worker.Emit(OpCodes.Stloc, index);
        }
        
        private static void Crement(int ammount, bool increment)
        {
            worker.Emit(OpCodes.Ldloc, array);                             // load this
            worker.Emit(OpCodes.Ldloc, index);                             // and this for the stelem

            worker.Emit(OpCodes.Ldloc, array);                             // this
            worker.Emit(OpCodes.Ldloc, index);                             // and this get popped
            worker.Emit(intArray ? OpCodes.Ldelem_I4 : OpCodes.Ldelem_I1); // right now
            worker.Emit(OpCodes.Ldc_I4, ammount);
            worker.Emit(increment ? OpCodes.Add : OpCodes.Sub);
            worker.Emit(intArray ? OpCodes.Conv_I4 : OpCodes.Conv_U1);
            
            worker.Emit(intArray ? OpCodes.Stelem_I4 : OpCodes.Stelem_I1); // and here's that stelem
        }
        
        private static bool IsCommandChar(char c)
        {
            return (c == '>' || c == '<' || c == '+' || c == '-' || c == '.' || c == ',' || c == '[' || c == ']');
        }
        
        public static string Version {
            get { return "1.01"; }
        }
    }
}